/*
 * FeedRepresentation.java
 *
 * Created on March 27, 2007, 11:55 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.atomojo.app;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.sql.SQLException;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.logging.Level;
import org.atomojo.app.client.XMLRepresentationParser;
import org.atomojo.app.db.Entry;
import org.atomojo.app.db.Feed;
import org.atomojo.app.db.Term;
import org.atomojo.app.db.TermInstance;
import org.atomojo.app.db.sparql.QueryContext;
import org.atomojo.app.db.sparql.QueryException;
import org.atomojo.sparql.ParseException;
import org.atomojo.sparql.Parser;
import org.atomojo.sparql.Query;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.InfosetFactory;
import org.infoset.xml.ItemConstructor;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.infoset.xml.filter.RemoveDocumentFilter;
import org.infoset.xml.sax.SAXDocumentLoader;
import org.infoset.xml.util.WriterItemDestination;
import org.restlet.data.CharacterSet;
import org.restlet.data.MediaType;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.representation.OutputRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.resource.ServerResource;

/**
 *
 * @author alex
 */
public class TermListResource extends ServerResource {

   static final URI CHILD_TERM = URI.create("http://www.atomojo.org/O/relations/child");
   static final URI ANCESTOR_TERM = URI.create("http://www.atomojo.org/O/relations/ancestor");
   Reference termBase;
   Reference resourceBase;
   App app;
   class TermQuery {
      Term dbTerm;
      String value;
      TermQuery(URI u,String value) 
         throws SQLException
      {
         this.value = value;
         this.dbTerm = app.getDB().findTerm(u);
      }
   }
   
   /** Creates a new instance of FeedRepresentation */
   public TermListResource(Reference termBase,Reference resourceBase,App app) {
      setNegotiated(false);
      this.termBase = termBase;
      this.resourceBase = resourceBase;
      this.app = app;
   }
   
   public Representation get() {
      return new OutputRepresentation(MediaType.APPLICATION_ATOM_XML) {
         public void write(OutputStream os)
            throws IOException
         {
            setCharacterSet(CharacterSet.UTF_8);
            try {
               generate(new WriterItemDestination(new OutputStreamWriter(os,"UTF-8"),"UTF-8"));
            } catch (XMLException ex) {
               getContext().getLogger().log(Level.SEVERE,"Cannot serialize metadata feed.",ex);
               throw new IOException("Cannot serialize metadata feed: "+ex.getMessage());
            }
         }
      };
   }
   
   public void generate(ItemDestination dest)
      throws XMLException
   {
      Date now = new Date();
      ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
      dest.send(constructor.createDocument());
      dest.send(constructor.createElement(AtomResource.FEED_NAME));
      link(constructor,dest,"self",getRequest().getResourceRef().toString());
      text(constructor,dest,AtomResource.TITLE_NAME,"Terms");
      text(constructor,dest,AtomResource.ID_NAME,getRequest().getResourceRef().toString());
      text(constructor,dest,AtomResource.UPDATED_NAME,AtomResource.toXSDDate(now));
      try {
         Iterator<Term> terms = app.getDB().getTerms();
         while (terms.hasNext()) {
            Term t = terms.next();
            dest.send(constructor.createElement(AtomResource.ENTRY_NAME));
            String termRef = termBase.toString()+t.getURI();
            text(constructor,dest,AtomResource.ID_NAME,termRef);
            text(constructor,dest,AtomResource.PUBLISHED_NAME,AtomResource.toXSDDate(now));
            text(constructor,dest,AtomResource.UPDATED_NAME,AtomResource.toXSDDate(now));
            text(constructor,dest,AtomResource.TITLE_NAME,"Term "+t.getURI());
            term(constructor,dest,Categorization.TERM_TYPE_TERM,null);
            term(constructor,dest,t.getURI(),null);
            link(constructor,dest,"related",termRef);
            dest.send(constructor.createElementEnd(AtomResource.ENTRY_NAME));
         }
      } catch (Exception ex) {
         throw new XMLException("Exception while getting terms.",ex);
      }
      dest.send(constructor.createElementEnd(AtomResource.FEED_NAME));
      dest.send(constructor.createDocumentEnd());
   }
   
   public Representation post(Representation entity) {
      if (entity==null) {
         getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         return new StringRepresentation("Missing post body.");
      }
      MediaType postType = entity.getMediaType();
      
      if (postType.getName().equals("application/sparql-query")) {
         return handleSparqlPost(entity);
      } else if (!postType.getName().equals(MediaType.APPLICATION_XML.getName())) {
         getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         return new StringRepresentation("Non-xml media type "+postType.getName());
      }
      String charset = postType.getParameters().getValues("charset");
      if (charset==null) {
         charset = "UTF-8";
      }
      Document doc = null;
      DocumentLoader loader = new SAXDocumentLoader();
      try {
         Reader r = new InputStreamReader(entity.getStream(),charset);
         doc = loader.load(r);
         r.close();
      } catch (IOException ex) {
         getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         return new StringRepresentation("I/O error while parsing document: "+ex.getMessage());
      } catch (XMLException ex) {
         getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         return new StringRepresentation("XML error while parsing document: "+ex.getMessage());
      }
      Element top = doc.getDocumentElement();
      if (!top.getName().equals(AtomResource.CATEGORIES_NAME)) {
         getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         return new StringRepresentation("Unexpected document element "+top.getName());
      }
      URI firstTerm = null;
      final Map<URI,TermQuery> termSet = new TreeMap<URI,TermQuery>();
      Iterator<Element> categories = top.getElementsByName(AtomResource.CATEGORY_NAME);
      try {
         while (categories.hasNext()) {
            org.atomojo.app.client.Term term = org.atomojo.app.client.Term.derive(categories.next());
            URI termURI = term.getURI();
            String value = term.getFirstValue();
            termSet.put(termURI, new TermQuery(termURI,value));
            if (firstTerm==null) {
               firstTerm = termURI;
            }
         }
      } catch (SQLException ex) {
         getLogger().log(Level.SEVERE,"Database error while getting term.",ex);
         getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
         return new StringRepresentation("Database error.");
      }
      if (firstTerm==null) {
         getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         return new StringRepresentation("Empty category set.");
      }
      final URI keyTerm = firstTerm;
      getResponse().setStatus(Status.SUCCESS_OK);
      return new OutputRepresentation(MediaType.APPLICATION_ATOM) {
         public void write(OutputStream os)
            throws IOException
         {
            setCharacterSet(CharacterSet.UTF_8);
            try {
               generate(new WriterItemDestination(new OutputStreamWriter(os,"UTF-8"),"UTF-8"),keyTerm,termSet);
            } catch (XMLException ex) {
               getContext().getLogger().log(Level.SEVERE,"Cannot serialize metadata feed.",ex);
               throw new IOException("Cannot serialize metadata feed: "+ex.getMessage());
            }
         }
      };
   }
   
   public Representation handleSparqlPost(Representation entity) {
      MediaType postType = entity.getMediaType();
      String charset = postType.getParameters().getValues("charset");
      if (charset==null) {
         charset = "UTF-8";
      }
      try {
         Reader r = new InputStreamReader(entity.getStream(),charset);
         Parser parser = new Parser(getLogger());
         Query query = parser.parseQuery(r);
         r.close();

         getResponse().setStatus(Status.SUCCESS_OK);
         final QueryContext context = new QueryContext(getLogger(),app.getDB(),query);
         final XMLRepresentationParser xmlParser = new XMLRepresentationParser();
         return new OutputRepresentation(MediaType.APPLICATION_ATOM) {
            public void write(OutputStream os)
               throws IOException
            {
               final ItemDestination dest = new WriterItemDestination(new OutputStreamWriter(os,"UTF-8"),"UTF-8");
               final ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
               try {
                  context.execute(new QueryContext.ResultListener() {
                     DocumentLoader loader = new SAXDocumentLoader();
                     public void onStart() throws QueryException {
                        try {
                           dest.send(constructor.createDocument());
                           dest.send(constructor.createElement(AtomResource.FEED_NAME));
                           dest.send(constructor.createCharacters("\n"));
                           link(constructor,dest,"self",getRequest().getResourceRef().toString());
                           dest.send(constructor.createCharacters("\n"));
                           text(constructor,dest,AtomResource.TITLE_NAME,"Term Query");
                           dest.send(constructor.createCharacters("\n"));
                           text(constructor,dest,AtomResource.ID_NAME,UUID.randomUUID().toString());
                           dest.send(constructor.createCharacters("\n"));
                           text(constructor,dest,AtomResource.UPDATED_NAME,AtomResource.toXSDDate(new Date()));
                        } catch (XMLException ex) {
                           throw new QueryException("Exception during feed start.",ex);
                        }
                     }

                     public void onEnd() throws QueryException {
                        try {
                           dest.send(constructor.createCharacters("\n"));
                           dest.send(constructor.createElementEnd(AtomResource.FEED_NAME));
                           dest.send(constructor.createDocumentEnd());
                        } catch (XMLException ex) {
                           throw new QueryException("Exception during feed end.",ex);
                        }
                     }

                     public void onEntry(Entry entry) throws QueryException {
                        try {
                           Feed feed = entry.getFeed();
                           String feedPath = feed.getPath();
                           String feedBaseURI = resourceBase.toString()+feedPath;
                           dest.send(constructor.createCharacters("\n"));

                           // get the entry representation
                           Representation rep = app.getStorage().getEntry(feedBaseURI,feedPath,feed.getUUID(),entry.getUUID());

                           // avoid thread creation because reading an output representation requires a thread
                           StringWriter sw = new StringWriter();
                           rep.write(sw);
                           rep.release();

                           // TODO: optimize by giving item destination to storage
                           loader.generate(new StringReader(sw.toString()), new RemoveDocumentFilter(dest));
                        } catch (Exception ex) {
                           throw new QueryException("Exception during feed entry generation.",ex);
                        }
                     }

                     public void onFeed(Feed feed) throws QueryException {
                        try {
                           org.atomojo.app.client.Feed feedRep = new org.atomojo.app.client.Feed(xmlParser.load(app.getStorage().getFeedHead(feed.getPath(), feed.getUUID())));
                           dest.send(constructor.createCharacters("\n"));
                           dest.send(constructor.createElement(AtomResource.ENTRY_NAME));
                           text(constructor,dest,AtomResource.ID_NAME,feed.getUUID().toString());
                           text(constructor,dest,AtomResource.PUBLISHED_NAME,AtomResource.toXSDDate(feed.getCreated()));
                           text(constructor,dest,AtomResource.UPDATED_NAME,AtomResource.toXSDDate(feed.getEdited()));
                           text(constructor,dest,AtomResource.TITLE_NAME,feedRep.getTitle());
                           String summary = feedRep.getSummary();
                           text(constructor,dest,AtomResource.SUMMARY_NAME,summary);
                           term(constructor,dest,Categorization.FEED_TYPE_TERM,null);
                           Iterator<TermInstance<Feed>> terms = feed.getTerms();
                           while (terms.hasNext()) {
                              TermInstance<Feed> fterm = terms.next();
                              term(constructor,dest,fterm.getTerm().getURI(),fterm.getValue());
                           }
                           link(constructor,dest,"related",resourceBase+feed.getPath());
                           dest.send(constructor.createElementEnd(AtomResource.ENTRY_NAME));
                        } catch (Exception ex) {
                           throw new QueryException("Exception during feed entry generation.",ex);
                        }
                     }
                  } );
               } catch (QueryException ex) {
                  getContext().getLogger().log(Level.SEVERE,"Cannot execute query",ex);
                  throw new IOException("Cannot execute query: "+ex.getMessage());
               }
               
            }
         };
      } catch (ParseException ex) {
         getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         return new StringRepresentation("Error while parsing query: "+ex.getMessage());
      } catch (QueryException ex) {
         getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         return new StringRepresentation("Error while processing query: "+ex.getMessage());
      } catch (IOException ex) {
         getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
         return new StringRepresentation("I/O exception while parsing query.");
      }
      
   }
   
   public void generate(ItemDestination dest,URI keyTerm,Map<URI,TermQuery> termSet)
      throws XMLException
   {
      TermQuery key = termSet.remove(keyTerm);
      boolean found = key.dbTerm!=null;
      for (URI u : termSet.keySet()) {
         TermQuery t = termSet.get(u);
         if (t.dbTerm==null) {
            found = false;
            break;
         }
      }
      
      ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
      dest.send(constructor.createDocument());
      dest.send(constructor.createElement(AtomResource.FEED_NAME));
      dest.send(constructor.createCharacters("\n"));
      link(constructor,dest,"self",getRequest().getResourceRef().toString());
      dest.send(constructor.createCharacters("\n"));
      text(constructor,dest,AtomResource.TITLE_NAME,"Term Query");
      dest.send(constructor.createCharacters("\n"));
      text(constructor,dest,AtomResource.ID_NAME,getRequest().getResourceRef().toString());
      dest.send(constructor.createCharacters("\n"));
      text(constructor,dest,AtomResource.UPDATED_NAME,AtomResource.toXSDDate(new Date()));
      try {
         if (found) {
            Iterator<TermInstance<Feed>> feeds = app.getDB().getFeedsByTerm(key.dbTerm,key.value);
            while (feeds.hasNext()) {
               TermInstance<Feed> termValue = feeds.next();
               Feed feed = termValue.getTarget();
               boolean ok = true;
               for (URI otherTerm : termSet.keySet()) {
                  TermQuery other = termSet.get(otherTerm);
                  ok = feed.hasTerm(other.dbTerm, other.value);
                  if (!ok) {
                     break;
                  }
               }
               if (ok) {
                  dest.send(constructor.createCharacters("\n"));
                  dest.send(constructor.createElement(AtomResource.ENTRY_NAME));
                  text(constructor,dest,AtomResource.ID_NAME,feed.getUUID().toString());
                  text(constructor,dest,AtomResource.PUBLISHED_NAME,AtomResource.toXSDDate(feed.getCreated()));
                  text(constructor,dest,AtomResource.UPDATED_NAME,AtomResource.toXSDDate(feed.getEdited()));
                  text(constructor,dest,AtomResource.TITLE_NAME,app.getStorage().getFeedTitle(feed.getPath(),feed.getUUID()));
                  term(constructor,dest,Categorization.FEED_TYPE_TERM,null);
                  Iterator<TermInstance<Feed>> terms = feed.getTerms();
                  while (terms.hasNext()) {
                     TermInstance<Feed> fterm = terms.next();
                     term(constructor,dest,fterm.getTerm().getURI(),fterm.getValue());
                  }
                  link(constructor,dest,"related",resourceBase+feed.getPath());
                  dest.send(constructor.createElementEnd(AtomResource.ENTRY_NAME));
               }
            }
            DocumentLoader loader = new SAXDocumentLoader();
            Iterator<TermInstance<Entry>> entries = app.getDB().getEntriesByTerm(key.dbTerm,key.value);
            while (entries.hasNext()) {
               TermInstance<Entry> termValue = entries.next();
               Entry entry = termValue.getTarget();
               boolean ok = true;
               for (URI otherTerm : termSet.keySet()) {
                  TermQuery other = termSet.get(otherTerm);
                  ok = entry.hasTerm(other.dbTerm, other.value);
                  if (!ok) {
                     break;
                  }
               }
               if (ok) {
                  Feed feed = entry.getFeed();
                  String feedPath = feed.getPath();
                  String feedBaseURI = resourceBase.toString()+feedPath;
                  dest.send(constructor.createCharacters("\n"));

                  // get the entry representation
                  Representation rep = app.getStorage().getEntry(feedBaseURI,feedPath,feed.getUUID(),entry.getUUID());

                  // avoid thread creation because reading an output representation requires a thread
                  StringWriter sw = new StringWriter();
                  rep.write(sw);
                  rep.release();
                  loader.generate(new StringReader(sw.toString()), new RemoveDocumentFilter(dest));
               }
               
            }
         }
      } catch (Exception ex) {
         throw new XMLException("Exception while getting ancestors.",ex);
      }
      dest.send(constructor.createCharacters("\n"));
      dest.send(constructor.createElementEnd(AtomResource.FEED_NAME));
      dest.send(constructor.createDocumentEnd());
   }
   
   
   
   static void text(ItemConstructor constructor,ItemDestination dest,Name name,String text)
      throws XMLException
   {
      dest.send(constructor.createElement(name));
      dest.send(constructor.createCharacters(text));
      dest.send(constructor.createElementEnd(name));
   }
      
   static void term(ItemConstructor constructor,ItemDestination dest,URI term,Object value)
      throws XMLException
   {
      String [] parts = new String[2];
      Term.split(term,parts);
      try {
         Element termE = constructor.createElement(AtomResource.CATEGORY_NAME);
         if (!parts[0].equals("http://www.atomojo.org/O/keyword/")) {
            termE.setAttributeValue("scheme",parts[0]);
         }
         termE.setAttributeValue("term",URLDecoder.decode(parts[1],"UTF-8"));
         dest.send(termE);
         if (value!=null) {
            dest.send(constructor.createCharacters(value.toString()));
         }

         dest.send(constructor.createElementEnd(AtomResource.CATEGORY_NAME));
      } catch (UnsupportedEncodingException ex) {
         throw new XMLException("Cannot decode term "+parts[1],ex);
      }
   }
   static void link(ItemConstructor constructor,ItemDestination dest,String rel,String href)
      throws XMLException
   {
      Element linkE = constructor.createElement(AtomResource.LINK_NAME);
      linkE.setAttributeValue("rel",rel);
      linkE.setAttributeValue("href",href);
      linkE.setAttributeValue("type","application/atom+xml");
      dest.send(linkE);
      
      dest.send(constructor.createElementEnd(AtomResource.LINK_NAME));
   }
}
